/*
 * Copyright (c) 2009, 2017, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "base/string_utilities.h"
#include "base/file_functions.h"
#include "base/log.h"

#ifndef HAVE_PRECOMPILED_HEADERS
#include <stdexcept>
#include <functional>
#include <locale>
#include <algorithm>
#include <math.h>
#include <errno.h>
#include <string.h>
#include <fstream>
#include <boost/locale/encoding_utf.hpp>
#endif

DEFAULT_LOG_DOMAIN(DOMAIN_BASE);

// updated as of 5.7
static const char *reserved_keywords[] = {"ACCESSIBLE",
                                          "ADD",
                                          "ALL",
                                          "ALTER",
                                          "ANALYZE",
                                          "AND",
                                          "AS",
                                          "ASC",
                                          "ASENSITIVE",
                                          "BEFORE",
                                          "BETWEEN",
                                          "BIGINT",
                                          "BINARY",
                                          "BLOB",
                                          "BOTH",
                                          "BY",
                                          "CALL",
                                          "CASCADE",
                                          "CASE",
                                          "CHANGE",
                                          "CHAR",
                                          "CHARACTER",
                                          "CHECK",
                                          "COLLATE",
                                          "COLUMN",
                                          "CONDITION",
                                          "CONSTRAINT",
                                          "CONTINUE",
                                          "CONVERT",
                                          "CREATE",
                                          "CROSS",
                                          "CURRENT_DATE",
                                          "CURRENT_TIME",
                                          "CURRENT_TIMESTAMP",
                                          "CURRENT_USER",
                                          "CURSOR",
                                          "DATABASE",
                                          "DATABASES",
                                          "DAY_HOUR",
                                          "DAY_MICROSECOND",
                                          "DAY_MINUTE",
                                          "DAY_SECOND",
                                          "DEC",
                                          "DECIMAL",
                                          "DECLARE",
                                          "DEFAULT",
                                          "DELAYED",
                                          "DELETE",
                                          "DESC",
                                          "DESCRIBE",
                                          "DETERMINISTIC",
                                          "DISTINCT",
                                          "DISTINCTROW",
                                          "DIV",
                                          "DOUBLE",
                                          "DROP",
                                          "DUAL",
                                          "EACH",
                                          "ELSE",
                                          "ELSEIF",
                                          "ENCLOSED",
                                          "ESCAPED",
                                          "EXISTS",
                                          "EXIT",
                                          "EXPLAIN",
                                          "FALSE",
                                          "FETCH",
                                          "FLOAT",
                                          "FLOAT4",
                                          "FLOAT8",
                                          "FOR",
                                          "FORCE",
                                          "FOREIGN",
                                          "FROM",
                                          "FULLTEXT",
                                          "GET",
                                          "GRANT",
                                          "GROUP",
                                          "HAVING",
                                          "HIGH_PRIORITY",
                                          "HOUR_MICROSECOND",
                                          "HOUR_MINUTE",
                                          "HOUR_SECOND",
                                          "IF",
                                          "IGNORE",
                                          "IN",
                                          "INDEX",
                                          "INFILE",
                                          "INNER",
                                          "INOUT",
                                          "INSENSITIVE",
                                          "INSERT",
                                          "INT",
                                          "INT1",
                                          "INT2",
                                          "INT3",
                                          "INT4",
                                          "INT8",
                                          "INTEGER",
                                          "INTERVAL",
                                          "INTO",
                                          "IO_AFTER_GTIDS",
                                          "IO_BEFORE_GTIDS",
                                          "IS",
                                          "ITERATE",
                                          "JOIN",
                                          "KEY",
                                          "KEYS",
                                          "KILL",
                                          "LEADING",
                                          "LEAVE",
                                          "LEFT",
                                          "LIKE",
                                          "LIMIT",
                                          "LINEAR",
                                          "LINES",
                                          "LOAD",
                                          "LOCALTIME",
                                          "LOCALTIMESTAMP",
                                          "LOCK",
                                          "LONG",
                                          "LONGBLOB",
                                          "LONGTEXT",
                                          "LOOP",
                                          "LOW_PRIORITY",
                                          "MASTER_BIND",
                                          "MASTER_SSL_VERIFY_SERVER_CERT",
                                          "MATCH",
                                          "MAXVALUE",
                                          "MEDIUMBLOB",
                                          "MEDIUMINT",
                                          "MEDIUMTEXT",
                                          "MIDDLEINT",
                                          "MINUTE_MICROSECOND",
                                          "MINUTE_SECOND",
                                          "MOD",
                                          "MODIFIES",
                                          "NATURAL",
                                          "NONBLOCKING",
                                          "NOT",
                                          "NO_WRITE_TO_BINLOG",
                                          "NULL",
                                          "NUMERIC",
                                          "ON",
                                          "OPTIMIZE",
                                          "OPTION",
                                          "OPTIONALLY",
                                          "OR",
                                          "ORDER",
                                          "OUT",
                                          "OUTER",
                                          "OUTFILE",
                                          "PARTITION",
                                          "PRECISION",
                                          "PRIMARY",
                                          "PROCEDURE",
                                          "PURGE",
                                          "RANGE",
                                          "READ",
                                          "READS",
                                          "READ_WRITE",
                                          "REAL",
                                          "REFERENCES",
                                          "REGEXP",
                                          "RELEASE",
                                          "RENAME",
                                          "REPEAT",
                                          "REPLACE",
                                          "REQUIRE",
                                          "RESIGNAL",
                                          "RESTRICT",
                                          "RETURN",
                                          "REVOKE",
                                          "RIGHT",
                                          "RLIKE",
                                          "SCHEMA",
                                          "SCHEMAS",
                                          "SECOND_MICROSECOND",
                                          "SELECT",
                                          "SENSITIVE",
                                          "SEPARATOR",
                                          "SET",
                                          "SHOW",
                                          "SIGNAL",
                                          "SMALLINT",
                                          "SPATIAL",
                                          "SPECIFIC",
                                          "SQL",
                                          "SQLEXCEPTION",
                                          "SQLSTATE",
                                          "SQLWARNING",
                                          "SQL_BIG_RESULT",
                                          "SQL_CALC_FOUND_ROWS",
                                          "SQL_SMALL_RESULT",
                                          "SSL",
                                          "STARTING",
                                          "STRAIGHT_JOIN",
                                          "TABLE",
                                          "TERMINATED",
                                          "THEN",
                                          "TINYBLOB",
                                          "TINYINT",
                                          "TINYTEXT",
                                          "TO",
                                          "TRAILING",
                                          "TRIGGER",
                                          "TRUE",
                                          "UNDO",
                                          "UNION",
                                          "UNIQUE",
                                          "UNLOCK",
                                          "UNSIGNED",
                                          "UPDATE",
                                          "USAGE",
                                          "USE",
                                          "USING",
                                          "UTC_DATE",
                                          "UTC_TIME",
                                          "UTC_TIMESTAMP",
                                          "VALUES",
                                          "VARBINARY",
                                          "VARCHAR",
                                          "VARCHARACTER",
                                          "VARYING",
                                          "WHEN",
                                          "WHERE",
                                          "WHILE",
                                          "WITH",
                                          "WRITE",
                                          "XOR",
                                          "YEAR_MONTH",
                                          "ZEROFILL",
                                          NULL};

namespace base {

#ifdef _WIN32

  // Win uses C++11 with support for wstring_convert. Other platforms use boost for now.

  //--------------------------------------------------------------------------------------------------

  static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> utf16Converter;
  static std::wstring_convert<std::codecvt_utf8<__int32>, __int32> utf32Converter;

  /**
   * Converts an UTF-8 encoded string to an UTF-16 string.
   */
  std::wstring string_to_wstring(const std::string &s) {
    if (sizeof(wchar_t) > 2) {
      auto utf32String = utf32Converter.from_bytes(s);
      return std::wstring(utf32String.begin(), utf32String.end());
    } else
      return utf16Converter.from_bytes(s);
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Converts an UTF-16 encoded string to an UTF-8 string.
   */
  std::string wstring_to_string(const std::wstring &s) {
    if (sizeof(wchar_t) > 2)
      return utf32Converter.to_bytes((__int32 *)s.c_str());
    else
      return utf16Converter.to_bytes(s);
  }

  //--------------------------------------------------------------------------------------------------

  std::wstring path_from_utf8(const std::string &s) {
    return string_to_wstring(s);
  }

#else

  using boost::locale::conv::utf_to_utf;

  std::wstring string_to_wstring(const std::string &str) {
    return utf_to_utf<wchar_t>(str.c_str(), str.c_str() + str.size());
  }

  //--------------------------------------------------------------------------------------------------

  std::string wstring_to_string(const std::wstring &str) {
    if (sizeof(wchar_t) > 2)
      return utf_to_utf<char>((int32_t *)str.c_str(), (int32_t *)str.c_str() + str.size());
    else
      return utf_to_utf<char>(str.c_str(), str.c_str() + str.size());
  }

  //--------------------------------------------------------------------------------------------------

  std::string path_from_utf8(const std::string &s) {
    return s;
  }

#endif

  //--------------------------------------------------------------------------------------------------

  std::string string_to_path_for_open(const std::string &s) {
// XXX: convert from utf-8 to wide string and then back to utf-8?
//      How can this help in any way here?
#ifdef _WIN32
    std::wstring ws = string_to_wstring(s);
    int buflen = GetShortPathNameW(ws.c_str(), NULL, 0);
    if (buflen > 0) {
      wchar_t *buffer = g_new(wchar_t, buflen);
      if (GetShortPathNameW(ws.c_str(), buffer, buflen) > 0) {
        char *buffer2;
        buflen = WideCharToMultiByte(CP_UTF8, 0, buffer, buflen, NULL, 0, 0, 0);
        buffer2 = g_new(char, buflen);
        if (WideCharToMultiByte(CP_UTF8, 0, buffer, buflen, buffer2, buflen, 0, 0) == 0) {
          std::string path(buffer2);
          g_free(buffer2);
          g_free(buffer);
          return path;
        }
        g_free(buffer2);
      }
      g_free(buffer);
    }
    return s;
#else
    return s;
#endif
  }

  //--------------------------------------------------------------------------------------------------

  inline bool is_invalid_filesystem_char(int ch) {
    static const char invalids[] = "/?<>\\:*|\"^";

    return memchr(invalids, ch, sizeof(invalids) - 1) != NULL;
  }

  std::string sanitize_file_name(const std::string &s) {
    static const char *invalid_filenames[] = {"com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8",
                                              "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7",
                                              "lpt8", "lpt9", "con",  "nul",  "prn",  ".",    "..",   NULL};
    std::string out;

    for (std::string::const_iterator c = s.begin(); c != s.end(); ++c) {
      // utf-8 has the high-bit = 1, so we just copy those verbatim
      if ((unsigned char)*c >= 128 || isalnum(*c) || (ispunct(*c) && !is_invalid_filesystem_char(*c)))
        out.push_back(*c);
      else
        out.push_back('_');
    }

    // not valid under windows
    if (!out.empty() && (out[out.size() - 1] == ' ' || out[out.size() - 1] == '.'))
      out[out.size() - 1] = '_';

    for (const char **fn = invalid_filenames; *fn; ++fn) {
      if (strcmp(out.c_str(), *fn) == 0) {
        out.append("_");
        break;
      }
    }

    return out;
  }

  //--------------------------------------------------------------------------------------------------

  std::string trim_right(const std::string &s, const std::string &t) {
    std::string d(s);
    std::string::size_type i(d.find_last_not_of(t));
    if (i == std::string::npos)
      return "";
    else
      return d.erase(d.find_last_not_of(t) + 1);
  }

  //--------------------------------------------------------------------------------------------------

  std::string trim_left(const std::string &s, const std::string &t) {
    std::string d(s);
    return d.erase(0, s.find_first_not_of(t));
  }

  //--------------------------------------------------------------------------------------------------

  std::string trim(const std::string &s, const std::string &t) {
    std::string d(s);
    return trim_left(trim_right(d, t), t);
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Simple case conversion routine, which returns a new string.
   * Note: converting to lower can be wrong when the returned string is used for string comparison,
   * because in some cultures letter cases are more complicated. Use string_compare instead in such cases.
   */
  std::string tolower(const std::string &s) {
    char *str_down = g_utf8_strdown(s.c_str(), (gsize)s.length());
    std::string result(str_down);
    g_free(str_down);
    return result;
  }

  //--------------------------------------------------------------------------------------------------

  std::string toupper(const std::string &s) {
    char *str_up = g_utf8_strup(s.c_str(), (gsize)s.length());
    std::string result(str_up);
    g_free(str_up);
    return result;
  }

  //--------------------------------------------------------------------------------------------------

  std::string truncate_text(const std::string &s, int max_length) {
    if ((int)s.length() > max_length) {
      std::string shortened(s.substr(0, max_length));
      const char *prev = g_utf8_find_prev_char(shortened.c_str(), shortened.c_str() + (max_length - 1));
      if (prev) {
        shortened.resize(prev - shortened.c_str(), 0);
        shortened.append("...");
      }
      return shortened;
    }
    return s;
  }

  //--------------------------------------------------------------------------------------------------

  std::string sanitize_utf8(const std::string &s) {
    const char *end = 0;
    if (!g_utf8_validate(s.data(), (gsize)s.size(), &end))
      return std::string(s.data(), end);
    return s;
  }

  //--------------------------------------------------------------------------------------------------

  std::vector<std::string> split(const std::string &s, const std::string &sep, int count) {
    std::vector<std::string> parts;
    std::string ss = s;

    std::string::size_type p;

    if (s.empty())
      return parts;

    if (count == 0)
      count = -1;

    p = ss.find(sep);
    while (!ss.empty() && p != std::string::npos && (count < 0 || count > 0)) {
      parts.push_back(ss.substr(0, p));
      ss = ss.substr(p + sep.size());

      --count;
      p = ss.find(sep);
    }
    parts.push_back(ss);

    return parts;
  }

  //--------------------------------------------------------------------------------------------------

  std::vector<std::string> split_by_set(const std::string &s, const std::string &separator_set, int count) {
    std::vector<std::string> parts;
    std::string ss = s;

    std::string::size_type p;

    if (s.empty())
      return parts;

    if (count == 0)
      count = -1;

    p = ss.find_first_of(separator_set);
    while (!ss.empty() && p != std::string::npos && (count < 0 || count > 0)) {
      parts.push_back(ss.substr(0, p));
      ss = ss.substr(p + 1);

      --count;
      p = ss.find_first_of(separator_set);
    }
    parts.push_back(ss);

    return parts;
  }

  //--------------------------------------------------------------------------------------------------

  static void findUntil(const char elem, const std::string &str, const int sep, std::string::size_type &p,
                        std::string::size_type &pe, std::string::size_type &end, std::vector<std::string> &parts) {
    // keep going until we find closing '
    while (pe < end) {
      auto it = str[pe++];
      if (it == elem) {
        if (pe < end && str[pe] == elem)
          pe++;
        else
          break;
      } else if (it == '\\') {
        if (pe < end)
          pe++;
      }
    }
    parts.push_back(str.substr(p, pe - p));
    p = pe;
    // skip whitespace
    while (p < end && (str[p] == ' ' || str[p] == '\t' || str[p] == '\r' || str[p] == '\n'))
      p++;
    if (p < end) {
      if (str[p] != sep)
        logDebug("Error splitting string list\n");
      else
        p++;
    }
  }

  std::vector<std::string> split_token_list(const std::string &s, int sep) {
    std::vector<std::string> parts;
    std::string ss = s;

    std::string::size_type end = s.size(), pe, p = 0;

    {
      bool empty_pending = true;
      while (p < end) {
        empty_pending = false;
        switch (s[p]) {
          case '\'':
            pe = p + 1;
            findUntil('\'', s, sep, p, pe, end, parts);
            break;

          case '"':
            pe = p + 1;
            findUntil('"', s, sep, p, pe, end, parts);
            break;

          case ' ':
          case '\t':
            p++;
            break;

          default:
            // skip until separator
            pe = p;
            while (pe < end) {
              if (s[pe] == sep) {
                empty_pending = true;
                break;
              }
              pe++;
            }
            parts.push_back(trim_right(s.substr(p, pe - p)));
            p = pe + 1;
            // skip whitespace
            while (p < end && (s[p] == ' ' || s[p] == '\t' || s[p] == '\r' || s[p] == '\n'))
              p++;
            break;
        }
      }
      if (empty_pending)
        parts.push_back("");
    }

    return parts;
  }

  //--------------------------------------------------------------------------------------------------

  bool partition(const std::string &s, const std::string &sep, std::string &left, std::string &right) {
    std::string::size_type p = s.find(sep);
    if (p != std::string::npos) {
      left = s.substr(0, p);
      right = s.substr(p + sep.size());
      return true;
    }
    left = s;
    right = "";
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Returns the index of the given string in the given vector or -1 if not found.
   */
  int index_of(const std::vector<std::string> &list, const std::string &s) {
    std::vector<std::string>::const_iterator location = std::find(list.begin(), list.end(), s);
    if (location == list.end())
      return -1;
    return (int)(location - list.begin());
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Returns a string containing all characters beginning at "start" from the given string "id", which form
   * a valid, unqualified identifier. The returned identifier does not contain any quoting anymore.
   * Note: this function is UTF-8 safe as it skips over all characters except some which are guaranteed
   *       not to be part of any valid UTF-8 sequence.
   *
   * @param id The string to examine.
   * @param start The start position to search from.
   *
   * @result Returns the first found identifier starting at "start" or an empty string if nothing was
   *         found. Parameter "start" points to the first character after the found identifier.
   */
  std::string get_identifier(const std::string &id, std::string::const_iterator &start) {
    std::string::const_iterator token_end = id.end();
    bool is_symbol_quoted = false;
    for (std::string::const_iterator i = start, i_end = token_end; i != i_end; ++i) {
      if (i_end != token_end)
        break;
      switch (*i) {
        case '.':
          if (!is_symbol_quoted)
            token_end = i;
          break;
        case ' ':
          if (!is_symbol_quoted)
            token_end = i;
          break;
        case '\'':
        case '"':
        case '`':
          if (*i == *start) {
            if (i != start)
              token_end = i + 1;
            else
              is_symbol_quoted = true;
          }
          break;
      }
    }

    if (token_end - start < 2)
      is_symbol_quoted = false;
    std::string result(start, token_end);
    start = token_end;
    if (is_symbol_quoted)
      return result.substr(1, result.size() - 2);

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Splits the given string into identifier parts assuming a format as allowed by the MySQL syntax for
   * qualified identifiers, e.g. part1.part2.part3 (any of the parts might be quoted).
   * In addition to the traditional syntax also these enhancements are supported:
   * - Unlimited level of nesting.
   * - Quoting might be done using single quotes, double quotes and back ticks.
   *
   * If an identifier is not separated by a dot from the rest of the input then this is considered
   * invalid input and ignored. Only identifiers found until that syntax violation are returned.
   */
  std::vector<std::string> split_qualified_identifier(const std::string &id) {
    std::vector<std::string> result;
    std::string::const_iterator iterator = id.begin();
    std::string token;
    do {
      token = get_identifier(id, iterator);
      if (token == "")
        break;
      result.push_back(token);
    } while ((iterator != id.end()) && (*iterator++ == '.'));

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Removes the first path part from @path and returns this part as well as the shortend path.
   */
  std::string pop_path_front(std::string &path) {
    std::string::size_type p = path.find('/');
    std::string res;
    if (p == std::string::npos || p == path.length() - 1) {
      res = path;
      path.clear();
      return res;
    }
    res = path.substr(0, p);
    path = path.substr(p + 1);
    return res;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Removes the last path part from @path and returns this part as well as the shortend path.
   */
  std::string pop_path_back(std::string &path) {
    std::string::size_type p = path.rfind('/');
    std::string res;
    if (p == std::string::npos || p == path.length() - 1) {
      res = path;
      path.clear();
      return res;
    }
    res = path.substr(p + 1);
    path = path.substr(0, p);
    return res;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Helper routine to format a string into an STL string using the printf parameter syntax.
   */
  std::string strfmt(const char *fmt, ...) {
    va_list args;
    char *tmp;
    std::string ret;

    va_start(args, fmt);
    tmp = g_strdup_vprintf(fmt, args);
    va_end(args);

    ret = tmp;
    g_free(tmp);

    return ret;
  }

  //--------------------------------------------------------------------------------------------------

  BASELIBRARY_PUBLIC_FUNC std::string sizefmt(int64_t s, bool metric) {
    float one_kb;
    const char *unit;
    if (metric) {
      one_kb = 1000;
      unit = "B";
    } else {
      one_kb = 1024;
      unit = "iB"; // http://en.wikipedia.org/wiki/Binary_prefix
    }

    if (s < one_kb)
      return strfmt("%iB", (int)s);
    else {
      float value = s / one_kb;
      if (value < one_kb)
        return strfmt("%.02fK%s", value, unit);
      else {
        value /= one_kb;
        if (value < one_kb)
          return strfmt("%.02fM%s", value, unit);
        else {
          value /= one_kb;
          if (value < one_kb)
            return strfmt("%.02fG%s", value, unit);
          else {
            value /= one_kb;
            if (value < one_kb)
              return strfmt("%.02fT%s", value, unit);
            else
              return strfmt("%.02fP%s", value / one_kb, unit);
          }
        }
      }
    }
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Helper routine to strip a string into an STL string using the printf parameter syntax.
   */
  std::string strip_text(const std::string &text, bool left, bool right) { // TODO sigc rewrite it in std/boost way
    std::locale loc;
    std::function<bool(std::string::value_type)> is_space =
      std::bind(&std::isspace<std::string::value_type>, std::placeholders::_1, loc);

    std::string::const_iterator l_edge =
      !left ? text.begin()
            : std::find_if(text.begin(), text.end(),
                           std::bind(std::logical_not<bool>(), std::bind(is_space, std::placeholders::_1)));
    std::string::const_reverse_iterator r_edge =
      !right ? text.rbegin()
             : std::find_if(text.rbegin(), text.rend(),
                            std::bind(std::logical_not<bool>(), std::bind(is_space, std::placeholders::_1)));

    return std::string(l_edge, r_edge.base());
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * replaces a variable from a string in format %variable%
   * a filter can be passed to the variable as in %variable|filter%
   * supported filters are upper, lower and capitalize
   */
  std::string replaceVariable(const std::string &format, const std::string &variable, const std::string &value) {
    std::string result = format;
    std::string::size_type pos;

    for (;;) {
      std::string s;
      std::string::size_type end;

      pos = result.find(variable.substr(0, variable.size() - 1));
      if (pos == std::string::npos)
        break;

      end = result.find('%', pos + 1);
      if (end == std::string::npos) // bad format
        break;

      s = result.substr(pos + 1, end - pos - 1);

      std::string::size_type filter_pos = s.find("|");
      std::string filtered_value = value;

      if (filter_pos == std::string::npos) {
        if (s.length() != variable.length() - 2)
          break;
      } else if (filter_pos != variable.length() - 2)
        break;
      else {
        std::string filter = s.substr(filter_pos + 1, s.size() - filter_pos);

        if (filter.compare("capitalize") == 0) {
          gunichar ch = g_utf8_get_char(value.data());

          ch = g_unichar_toupper(ch);

          gchar *rest = g_utf8_find_next_char(value.data(), value.data() + value.size());
          char utf8[10];
          utf8[g_unichar_to_utf8(ch, utf8)] = 0;
          filtered_value = std::string(utf8).append(rest);
        } else if (filter.compare("uncapitalize") == 0) {
          gunichar ch = g_utf8_get_char(value.data());

          ch = g_unichar_tolower(ch);

          gchar *rest = g_utf8_find_next_char(value.data(), value.data() + value.size());
          char utf8[10];
          utf8[g_unichar_to_utf8(ch, utf8)] = 0;
          filtered_value = std::string(utf8).append(rest);
        } else if (filter.compare("lower") == 0) {
          gchar *l = g_utf8_strdown(value.data(), (gssize)value.size());
          if (l)
            filtered_value = l;
          g_free(l);
        } else if (filter.compare("upper") == 0) {
          gchar *l = g_utf8_strup(value.data(), (gssize)value.size());
          if (l)
            filtered_value = l;
          g_free(l);
        }
      }
      result = result.substr(0, pos).append(filtered_value).append(result.substr(end + 1));
    }

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Add the given extension to the filename, if necessary.
   *
   */
  std::string normalize_path_extension(std::string filename, std::string extension) {
    if (!extension.empty() && !filename.empty()) {
      std::string::size_type p = filename.rfind('.');
      std::string old_extension = p != std::string::npos ? filename.substr(p) : "";

      if (old_extension.find('/') != std::string::npos || old_extension.find('\\') != std::string::npos)
        old_extension.clear();

      if (!extension.empty() && extension[0] != '.')
        extension = "." + extension;

      if (old_extension.empty())
        filename.append(extension);
      else {
        if (old_extension != extension)
          filename = filename.substr(0, p).append(extension);
      }
    }
    return filename;
  }

  /**
   * Removes all unnecessary path separators as well as "./" combinations.
   * If there is a parent-dir entry (../) then this as well as the directly prefacing
   * dir entry is removed.
   */
  std::string normalize_path(const std::string path) {
    // First convert all separators to the one that is used on the platform (no mix)
    // and ease so at the same time further processing here.
    std::string result;
    std::string separator(1, G_DIR_SEPARATOR);

    result = path;
    replaceStringInplace(result, "\\", separator);
    replaceStringInplace(result, "/", separator);

    std::string double_separator = separator + separator;
    while (result.find(double_separator) != std::string::npos)
      replaceStringInplace(result, double_separator, separator);

    // Sanity check. Return *after* we have converted the slashs. This is part of the normalization.
    if (result.size() < 2)
      return result;

    std::vector<std::string> parts = split(result, separator);

    // Construct result backwards while examining the path parts.
    result = "";
    int pending_count = 0;
    for (ssize_t i = parts.size() - 1; i >= 0; i--) {
      if (parts[i].compare(".") == 0)
        // References to the current directory can be removed without further change.
        continue;

      if (parts[i].compare("..") == 0) {
        // An entry that points back to the parent dir.
        // Ignore this and keep track for later removal of the parent dir.
        pending_count++;
      } else if (pending_count > 0) {
        // If this is a normal dir entry and we have pending parent-dir redirections
        // then go one step up by removing (ignoring) this entry.
        pending_count--;
      } else
        result = separator + parts[i] + result;
    }

    // Don't return the leading separator.
    return result.substr(1);
  }

  std::string expand_tilde(const std::string &path) {
    if (!path.empty() && path[0] == '~' && (path.size() == 1 || path[1] == G_DIR_SEPARATOR)) {
      const char *homedir = g_getenv("HOME");
      if (!homedir)
        homedir = g_get_home_dir();

      return std::string(homedir).append(path.substr(1));
    }
    return path;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Checks the input for characters not allowed in the file system and converts them to underscore.
   */
  std::string make_valid_filename(const std::string &name) {
    std::string result;
    std::string illegal_chars = "\\/:?\"<>|*";
    for (std::string::const_iterator iterator = name.begin(); iterator != name.end(); ++iterator) {
      if (illegal_chars.find(*iterator) != std::string::npos)
        result += '_';
      else
        result += *iterator;
    }
    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Get a string containing the 'len' left most characters.
   */
  std::string left(const std::string &s, size_t len) {
    return s.substr(0, len);
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Get a string containing the 'len' right most characters.
   */
  std::string right(const std::string &s, size_t len) {
    if (len > s.size())
      len = s.size();
    if (len < 1)
      return "";

    return s.substr(std::max(s.length() - len, (size_t)0));
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Tests if s begins with part.
   */
  bool hasPrefix(const std::string &s, const std::string &part) {
    return s.compare(0, part.length(), part) == 0;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Tests if s ends with part.
   */
  bool hasSuffix(const std::string &s, const std::string &part) {
    int start_at = (int)s.length() - (int)part.length();

    // If start_at < 0 then the search string is bigger then the source, so the results is false.
    // On the other hand, if it starts after the end, something went wrong...
    if (start_at < 0 || start_at > (int)s.length())
      return false;

    return s.compare(start_at, std::string::npos, part) == 0;
  }
  //--------------------------------------------------------------------------------------------------

  void replaceStringInplace(std::string &value, const std::string &search, const std::string &replacement) {
    std::string::size_type next;

    for (next = value.find(search); next != std::string::npos; next = value.find(search, next)) {
      value.replace(next, search.length(), replacement);
      next += replacement.length();
    }
  }

  //--------------------------------------------------------------------------------------------------

  std::string replaceString(const std::string &s, const std::string &from, const std::string &to) {
    std::string::size_type p;
    std::string ss, res;

    ss = s;
    p = ss.find(from);
    while (p != std::string::npos) {
      if (p > 0)
        res.append(ss.substr(0, p)).append(to);
      else
        res.append(to);
      ss = ss.substr(p + from.size());
      p = ss.find(from);
    }
    res.append(ss);

    return res;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Write text data to file, converting to \r\n if in Windows.
   */
  void setTextFileContent(const std::string &filename, const std::string &data) {
#ifdef _WIN32
    // Opening a file in text mode will automatically convert \n to \r\n.
    FILE *f = base_fopen(filename.c_str(), "w+t");
    if (!f)
      throw std::runtime_error(g_strerror(errno));

    size_t bytes_written = fwrite(data.data(), 1, data.size(), f);
    fclose(f);
    if (bytes_written != data.size())
      throw std::runtime_error(g_strerror(errno));
#else
    GError *error = NULL;
    g_file_set_contents(filename.c_str(), data.data(), data.size(), &error);
    if (error) {
      std::string msg = error->message;
      g_error_free(error);
      throw std::runtime_error(msg);
    }
#endif
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Reads text data from the given file (file name encoded as utf-8) and returns the content as utf-8.
   * It can read ASCII/ANSI, utf-8 and utf-16 files (LE only) with and without BOM (BOM not included in result).
   */
  std::string getTextFileContent(const std::string &filename) {
    enum Encoding { ANSI, UTF8, UTF16LE } encoding = ANSI;

    std::string result;
#ifdef _WIN32
    std::ifstream stream(string_to_wstring(filename).c_str(), std::ios::binary);
#else
    std::ifstream stream(filename.c_str(), std::ifstream::binary);
#endif
    std::stringstream ss;

    if (!stream.is_open() || stream.eof())
      return "";

    int ch1 = stream.get();
    int ch2 = stream.get();
    if (ch1 == 0xff && ch2 == 0xfe)
      encoding = UTF16LE;
    else if (ch1 == 0xfe && ch2 == 0xff)
      return "UTF-16BE not supported";
    else {
      int ch3 = stream.get();
      if (ch1 == 0xef && ch2 == 0xbb && ch3 == 0xbf)
        encoding = UTF8;
      else
        stream.seekg(0);
    }

    ss << stream.rdbuf() << '\0';
    switch (encoding) {
      case UTF16LE:
        return wstring_to_string(std::wstring((wchar_t *)ss.str().c_str()));
      default:
        return ss.str();
    }
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Escape a string to be used in a SQL query
   * Same code as used by mysql. Handles null bytes in the middle of the string.
   * If wildcards is true then _ and % are masked as well.
   */
  std::string escape_sql_string(const std::string &s, bool wildcards) {
    std::string result;
    result.reserve(s.size());

    for (std::string::const_iterator ch = s.begin(); ch != s.end(); ++ch) {
      char escape = 0;

      switch (*ch) {
        case 0: /* Must be escaped for 'mysql' */
          escape = '0';
          break;
        case '\n': /* Must be escaped for logs */
          escape = 'n';
          break;
        case '\r':
          escape = 'r';
          break;
        case '\\':
          escape = '\\';
          break;
        case '\'':
          escape = '\'';
          break;
        case '"': /* Better safe than sorry */
          escape = '"';
          break;
        case '\032': /* This gives problems on Win32 */
          escape = 'Z';
          break;
        case '_':
          if (wildcards)
            escape = '_';
          break;
        case '%':
          if (wildcards)
            escape = '%';
          break;
      }
      if (escape) {
        result.push_back('\\');
        result.push_back(escape);
      } else
        result.push_back(*ch);
    }
    return result;
  }

  /**
   * Escape a string to be used in a JSON
   */
  std::string escape_json_string(const std::string &s) {
    std::string result;
    result.reserve(s.size());
    for (auto ch : s) {
      char escape = 0;
      switch (ch) {
        case '"':
          escape = '"';
          break;
        case '\\':
          escape = '\\';
          break;
        case '\b':
          escape = 'b';
          break;
        case '\f':
          escape = 'f';
          break;
        case '\n':
          escape = 'n';
          break;
        case '\r':
          escape = 'r';
          break;
        case '\t':
          escape = 't';
          break;
        default:
          break;
      }
      if (escape) {
        result.push_back('\\');
        result.push_back(escape);
      } else
        result.push_back(ch);
    }
    return result;
  }

  /**
   * Removes repeated quote chars and supported escape sequences from the given string.
   * Invalid escape sequences are handled like in the server, by dropping the backslash and
   * using the wrong char as normal char.
   * The outer quoting stays intact and is not removed.
   */
  std::string unescape_sql_string(const std::string &s, char quote_char) {
    // Early out if the string is simply empty but quoted.
    if (s.size() == 2 && s[0] == quote_char && s[1] == quote_char)
      return s;

    std::string result;
    result.reserve(s.size());

    bool pendingQuote = false;
    bool pendingEscape = false;
    for (auto c : s) {
      if (!pendingEscape && c == quote_char) {
        if (pendingQuote)
          pendingQuote = false;
        else {
          pendingQuote = true;
          continue;
        }
      } else {
        if (pendingQuote) {
          pendingQuote = false;
          result.push_back(quote_char);
        }

        if (pendingEscape) {
          pendingEscape = false;
          switch (c) {
            case 'n':
              c = '\n';
              break;
            case 't':
              c = '\t';
              break;
            case 'r':
              c = '\r';
              break;
            case 'b':
              c = '\b';
              break;
            case '0':
              c = 0;
              break; // ASCII null
            case 'Z':
              c = '\032';
              break; // Win32 end of file
          }
        } else if (c == '\\') {
          pendingEscape = true;
          continue;
        }
      }
      result.push_back(c);
    }

    if (pendingQuote)
      result.push_back(quote_char);
    if (pendingEscape)
      result.push_back('\\');

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  // NOTE: This is not the same as escape_sql_string, as embedded ` must be escaped as ``, not \`
  // and \ ' and " must not be escaped
  std::string escape_backticks(const std::string &s) {
    std::string result;
    result.reserve(s.size());

    for (std::string::const_iterator ch = s.begin(); ch != s.end(); ++ch) {
      char escape = 0;

      switch (*ch) {
        case 0: /* Must be escaped for 'mysql' */
          escape = '0';
          break;
        case '\n': /* Must be escaped for logs */
          escape = 'n';
          break;
        case '\r':
          escape = 'r';
          break;
        case '\032': /* This gives problems on Win32 */
          escape = 'Z';
          break;
        case '`':
          // special case
          result.push_back('`');
          break;
      }
      if (escape) {
        result.push_back('\\');
        result.push_back(escape);
      } else
        result.push_back(*ch);
    }
    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Parses the given command line (which must be a usual mysql start command) and extracts the
   * value for the given parameter. The function can only return options of the form "option-name = option-value"
   * (both quoted and unquoted).
   */
  std::string extract_option_from_command_line(const std::string &option, const std::string &command_line) {
    std::string result;
    size_t position = command_line.find(option);
    if (position != std::string::npos) {
      position += option.size(); // Skip option name and find equal sign.
      while (position < command_line.size() && command_line[position] != '=')
        position++;

      if (command_line[position] == '=') {
        position++;

        // Skip any white space.
        while (position < command_line.size() && command_line[position] == ' ')
          position++;

        char terminator;
        if (command_line[position] == '"' || command_line[position] == '\'')
          terminator = command_line[position++];
        else
          terminator = ' ';

        size_t end_position = command_line.find(terminator, position);
        if (end_position == std::string::npos) {
          // Terminator not found means the string was either not properly terminated (if quoted)
          // or contains no space char. In this case take everything we can get.
          if (terminator != ' ')
            position++;
          result = command_line.substr(position);
        } else
          result = command_line.substr(position, end_position - position);
      }
    }
    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Splits the given font description and returns its details in the provided fields.
   *
   * @return True if successful, otherwise false.
   */
  bool parse_font_description(const std::string &fontspec, std::string &font, float &size, bool &bold, bool &italic) {
    std::vector<std::string> parts = split(fontspec, " ");
    font = fontspec;
    size = 12;
    bold = false;
    italic = false;

    if (parts.empty())
      return false;

    for (std::vector<std::string>::iterator iter = parts.begin(); iter != parts.end(); ++iter) {
      float size_check = 0;
      if (sscanf(iter->c_str(), "%f", &size_check) == 1) {
        size = size_check;
        parts.erase(iter);
        break;
      }
    }
    /*
      if (!parts.empty() && sscanf(parts.back().c_str(), "%f", &size) == 1)
        parts.pop_back();*/

    for (int i = 0; i < 2 && !parts.empty(); i++) {
      if (g_ascii_strcasecmp(parts.back().c_str(), "bold") == 0) {
        bold = true;
        parts.pop_back();
      }

      if (g_ascii_strcasecmp(parts.back().c_str(), "italic") == 0) {
        italic = true;
        parts.pop_back();
      }
    }

    if (!parts.empty()) {
      font = parts[0];
      for (unsigned int i = 1; i < parts.size(); i++)
        font += " " + parts[i];
    }
    return true;
  }

  //--------------------------------------------------------------------------------------------------

  std::string unquote_identifier(const std::string &identifier) {
    int start = 0;
    int size = (int)identifier.size();

    if (size == 0)
      return "";

    if (identifier[0] == '"' || identifier[0] == '`')
      start++;

    if (identifier[size - 1] == '"' || identifier[size - 1] == '`')
      size--;

    size -= start;

    return identifier.substr(start, size);
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * @brief Remove outer quotes from any text.
   *
   * @param text Text to unquote
   * @return Return unqoted text.
   */
  std::string unquote(const std::string &text) {
    if (text.size() < 2)
      return text;

    if ((text[0] == '"' || text[0] == '`' || text[0] == '\'') && text[0] == text[text.size() - 1])
      return text.substr(1, text.size() - 2);
    return text;
  }

  //--------------------------------------------------------------------------------------------------

  std::string quote_identifier(const std::string &identifier, const char quote_char) {
    return quote_char + identifier + quote_char;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Quotes the given identifier, but only if it needs to be quoted.
   * http://dev.mysql.com/doc/refman/5.1/en/identifiers.html specifies what is allowed in unquoted identifiers.
   * Leading numbers are not strictly forbidden but discouraged as they may lead to ambiguous behavior.
   */
  std::string quote_identifier_if_needed(const std::string &ident, const char quote_char) {
    bool needs_quotation = is_reserved_word(ident); // check whether it's a reserved keyword
    size_t digits = 0;

    if (!needs_quotation) {
      for (std::string::const_iterator i = ident.begin(); i != ident.end(); ++i) {
        if ((*i >= 'a' && *i <= 'z') || (*i >= 'A' && *i <= 'Z') || (*i >= '0' && *i <= '9') || (*i == '_') ||
            (*i == '$') || ((unsigned char)(*i) > 0x7F)) {
          if (*i >= '0' && *i <= '9')
            digits++;

          continue;
        }
        needs_quotation = true;
        break;
      }
    }

    if (needs_quotation || digits == ident.length())
      return quote_char + ident + quote_char;
    else
      return ident;
  }

  bool is_number(const std::string &word) {
    if (word.empty())
      return false;
    size_t i = 0;
    if (word[0] == '-')
      i++;
    for (; i < word.size(); i++)
      if (!isdigit(word[i]))
        return false;
    return true;
  }

  //--------------------------------------------------------------------------------------------------

  /**
  * @brief Determine if a string is a boolean.
  *
  * @param text Text to check
  * @return Return true if given string is a boolean.
  **/
  bool isBool(const std::string &text) {
    std::string transformed;
    std::transform(text.begin(), text.end(), std::back_inserter(transformed), ::tolower);
    if (transformed.compare("true") != 0 && transformed.compare("false") != 0)
      return false;
    return true;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Function : stl_string_compare
   * Description : comparison function to be used on the sorting process
   * Return Value : following the STL requirements should return true if the
   *                first string is lower than the second
   */
  bool stl_string_compare(const std::string &first, const std::string &second, bool case_sensitive) {
    return string_compare(first, second, case_sensitive) < 0;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Culturally correct string comparison. Also properly compares different normalization forms.
   * For a large amount of strings this function is not very effective as it generates the sort keys
   * repeatedly (not to mention normalization).
   * So if we ever need sorting of 10000 strings we have to add a separate implementation.
   *
   * @param first, the left string to compare.
   * @param second, the right string to compare.
   * @result   0 - If the strings are equal.
   *         < 0 - If first sorts before second.
   *         > 0 - If second sorts before first.
   */
  int string_compare(const std::string &first, const std::string &second, bool case_sensitive) {
    int result = 0;

    gchar *left = g_utf8_normalize(first.c_str(), -1, G_NORMALIZE_DEFAULT);
    gchar *right = g_utf8_normalize(second.c_str(), -1, G_NORMALIZE_DEFAULT);
    if (!case_sensitive) {
      gchar *s1 = g_utf8_casefold(left, -1);
      gchar *s2 = g_utf8_casefold(right, -1);
      result = g_utf8_collate(s1, s2);
      g_free(s1);
      g_free(s2);
    } else
      result = g_utf8_collate(left, right);

    g_free(left);
    g_free(right);

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Convenience function to determine if 2 strings are the same. This works also for culturally
   * equal letters (e.g. german ß and ss) and any normalization form.
   */
  bool same_string(const std::string &first, const std::string &second, bool case_sensitive) {
    return string_compare(first, second, case_sensitive) == 0;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Determines if the given candidate is part of the given text. As with the string_compare matches
   * are culturally correct.
   */
  bool contains_string(const std::string &text, const std::string &candidate, bool case_sensitive) {
    if (text.size() == 0 || candidate.size() == 0)
      return false;

    gchar *hay_stack = g_utf8_normalize(text.c_str(), -1, G_NORMALIZE_DEFAULT);
    gchar *needle = g_utf8_normalize(candidate.c_str(), -1, G_NORMALIZE_DEFAULT);

    if (!case_sensitive) {
      gchar *temp = g_utf8_casefold(hay_stack, -1);
      g_free(hay_stack);
      hay_stack = temp;

      temp = g_utf8_casefold(needle, -1);
      g_free(needle);
      needle = temp;
    }

    gunichar start_char = g_utf8_get_char(needle);

    bool result = false;
    gchar *run = hay_stack;
    while (!result) {
      gchar *p = g_utf8_strchr(run, -1, start_char);
      if (p == NULL)
        break;

      // Found the start char in the remaining text. See if that part matches the needle.
      gchar *needle_run = needle;
      bool mismatch = false;
      for (size_t i = 0; i < candidate.size(); ++i, ++p, ++needle_run) {
        if (g_utf8_get_char(needle_run) != g_utf8_get_char(p)) {
          mismatch = true;
          break;
        }
      }
      if (mismatch)
        ++run;
      else
        result = true;
    }
    g_free(hay_stack);
    g_free(needle);

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  bool is_reserved_word(const std::string &word) {
    std::string upper = base::toupper(word);
    for (const char **kw = reserved_keywords; *kw != NULL; ++kw) {
      if (upper.compare(*kw) == 0)
        return true;
    }
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  EolHelpers::Eol_format EolHelpers::detect(const std::string &text) {
    std::string::size_type pos = text.find_first_of("\r\n");
    if (std::string::npos == pos)
      return default_eol_format();
    if ('\r' == text[pos])
      return ('\n' == text[pos + 1]) ? eol_crlf : eol_cr;
    else
      return eol_lf;
  }

  int EolHelpers::count_lines(const std::string &text) {
    Eol_format eol_format = detect(text);
    char eol_sym = (eol_cr == eol_format) ? '\r' : '\n';
    return (int)std::count(text.begin(), text.end(), eol_sym);
  }

  bool EolHelpers::check(const std::string &text) {
    std::string::size_type pos = text.find_first_of("\n\r");
    if (std::string::npos == pos)
      return true;
    Eol_format eol_format = detect(text);
    if (eol_lf == eol_format) {
      if (text.find("\r") != std::string::npos)
        return false;
    } else if (eol_cr == eol_format) {
      if (text.find("\n") != std::string::npos)
        return false;
    } else if (eol_crlf == eol_format) {
      do {
        if (('\n' == text[pos]) || ('\n' != text[pos + 1]))
          return false;
        ++pos;
        ++pos;
        pos = text.find_first_of("\n\r", pos);
      } while (std::string::npos != pos);
    }
    return true;
  }

  void EolHelpers::conv(const std::string &src_text, Eol_format src_eol_format, std::string &dest_text,
                        Eol_format dest_eol_format) {
    if (src_eol_format == dest_eol_format)
      throw std::logic_error("source and target line ending formats coincide, no need to convert");

    const std::string &src_eol = eol(src_eol_format);
    const std::string &dest_eol = eol(dest_eol_format);
    std::string::size_type src_eol_length = src_eol.size();

    if (dest_eol.size() != src_eol.size()) {
      dest_text.clear();
      int line_count = count_lines(src_text);
      size_t dest_size = src_text.size() + line_count * (dest_eol.size() - src_eol.size());
      dest_text.reserve(dest_size);
      std::string::size_type prev_pos = 0;
      std::string::size_type pos = 0;
      while ((pos = src_text.find(src_eol, pos)) != std::string::npos) {
        dest_text.append(src_text, prev_pos, pos - prev_pos).append(dest_eol);
        pos += src_eol_length;
        prev_pos = pos;
      }
      dest_text.append(src_text, prev_pos, std::string::npos);
    } else {
      dest_text = src_text;
      std::string::size_type pos = 0;
      while ((pos = dest_text.find(src_eol, pos)) != std::string::npos) {
        dest_text.replace(pos, src_eol_length, dest_eol);
        pos += src_eol_length;
      }
    }
  }

  void EolHelpers::fix(const std::string &src_text, std::string &dest_text, Eol_format eol_format) {
    const std::string &dest_eol = eol(eol_format);
    std::string::size_type dest_eol_length = dest_eol.size();

    dest_text.clear();
    if (eol_crlf == eol_format) {
      int cr_count = (int)std::count(src_text.begin(), src_text.end(), '\r');
      int lf_count = (int)std::count(src_text.begin(), src_text.end(), '\n');
      int crlf_count = 0;
      {
        std::string::size_type pos = 0;
        while ((pos = src_text.find(dest_eol, pos)) != std::string::npos) {
          ++crlf_count;
          pos += dest_eol_length;
        }
      }
      size_t dest_size = src_text.size() + (cr_count - crlf_count) + (lf_count - crlf_count);
      dest_text.reserve(dest_size);
    }

    std::string::size_type prev_pos = 0;
    std::string::size_type pos = 0;
    std::string crlf = "\r\n";
    while ((pos = src_text.find_first_of(crlf, pos)) != std::string::npos) {
      dest_text.append(src_text, prev_pos, pos - prev_pos).append(dest_eol);
      if (('\r' == src_text[pos]) && ('\n' == src_text[pos + 1]))
        ++pos;
      ++pos;
      prev_pos = pos;
    }
    dest_text.append(src_text, prev_pos, std::string::npos);
  }

  //--------------------------------------------------------------------------------------------------

  std::string reflow_text(const std::string &text, unsigned int line_length, const std::string &left_fill,
                          bool indent_first, unsigned int max_lines) {
    bool use_fill = true;
    const unsigned int minimum_text_length = 5;

    //  Check if the line length complies to the minimum required
    if (line_length < minimum_text_length)
      return "";

    //  Only use left_fill when it's small enough to fit in the line and make the function able
    //  to do what it has to do
    const unsigned int left_fill_length = (unsigned)left_fill.size();

    if (left_fill_length + minimum_text_length >= line_length)
      use_fill = false;

    //  Check for empty string...if we let it go, a left_fill will be inserted
    if (text.size() == 0)
      return "";

    //  Check if it's a valid utf8 string
    const char *invalid_data_ptr = NULL;

    if (g_utf8_validate(text.c_str(), (gsize)text.size(), &invalid_data_ptr) != TRUE)
      throw std::invalid_argument(std::string("base::reflow_text received an invalid string: ") + text);

    const std::string initial = (indent_first && use_fill) ? left_fill : "";
    const std::string new_line = use_fill ? std::string("\n") + left_fill : std::string("\n");
    std::string result = initial;

    const char *char_string = text.c_str();
    const char *iter = char_string;

    unsigned int space_position_source = 0;
    unsigned int line_char_counter = 0;
    unsigned int line_counter = 0;
    unsigned int char_count_after_space = 0;
    unsigned int text_real_length = use_fill ? line_length - left_fill_length : line_length;

    while (*iter) {
      //  Get the full utf8 char into the result string
      result += std::string(iter, g_utf8_skip[*(const guchar *)(iter)]);

      line_char_counter++;
      char_count_after_space++;

      if (g_unichar_isspace(*iter) && line_char_counter > left_fill_length) {
        space_position_source = (unsigned)(iter - char_string + 1);
        char_count_after_space = 0;
      }

      if (line_char_counter == text_real_length) {
        //  Check for special case when we have a word as big as a line
        if (char_count_after_space == text_real_length) {
          result += new_line;

          space_position_source += char_count_after_space;
          line_char_counter = char_count_after_space = 0;
        } else {
          //  Find last space character position in the result string
          unsigned int break_position =
            space_position_source + line_counter * (unsigned)new_line.size() + (unsigned)initial.size();

          //  Insert a \n in the right position, right after the space char(or at the end of the string)
          result.size() == break_position ? result += new_line : result.insert(break_position, new_line);

          //  Mark the characters that were already inserted after the new line
          line_char_counter = char_count_after_space;
        }

        if (++line_counter == max_lines) {
          result.resize(result.size() - char_count_after_space - new_line.size());
          result += "\n(...)";
          break;
        }
      }

      iter = g_utf8_next_char((gchar *)iter); //  Get the next char from the sequence
    }

#ifdef DEBUG
    if (g_utf8_validate(result.c_str(), result.size(), &invalid_data_ptr) != TRUE)
      throw std::logic_error(
        strfmt("base::reflow_text produced an invalid string:\nInput:\n%s\nOutput:\n%s", text.c_str(), result.c_str()));
#endif

    return result;
  }

} // namespace base
